package com.example.mq.server.dao;

import com.example.mq.common.BinaryTool;
import com.example.mq.common.MqException;
import com.example.mq.server.core.MSGQueue;
import com.example.mq.server.core.Message;

import java.io.*;
import java.util.LinkedList;
import java.util.Scanner;

/**
 * 通过这个类对硬盘的消息进行管理
 */
public class MessageFileManager {
    // 定义内部类，表示该队列的统计信息
    static public class Stat {
        public int totalCount;
        public int validCount;
    }

    public void init() {
        // 便于后续扩展
    }
    // 该方法用来获取文件所在目录和文件名
    private String getQueueDir(String queueName) {
        return "./data/" + queueName;
    }

    // 获取消息数据的文件路径
    private String getQueueDataPath(String queueName) {
        return getQueueDir(queueName) + "/queue_data.txt";
    }

    // 获取消息统计路径
    private String getQueueStatPath(String queueName) {
        return getQueueDir(queueName) + "/queue_stat.txt";
    }

    // 创建队列对应的文件和目录
    public void createQueueFiles(String queueName) throws IOException {
        // 1.创建队列对应的目录
        File baseDir = new File(getQueueDir(queueName));
        if (!baseDir.exists()) {
            // 目录不存在则创建
            boolean ok = baseDir.mkdirs();
            if (!ok) {
                // 创建不成功抛出异常
                throw new IOException("消息目录创建失败！baseDir = " + baseDir.getAbsolutePath());
            }
        }
        // 2.创建队列数据文件
        File queueDataFile = new File(getQueueDataPath(queueName));
        if (!queueDataFile.exists()) {
            boolean ok = queueDataFile.createNewFile();
            if (!ok) {
                throw new IOException("创建数据文件失败 queueDataFile" + queueDataFile.getAbsolutePath());
            }
        }
        // 3.创建队列统计文件
        File queueStatFile = new File(getQueueStatPath(queueName));
        if (!queueStatFile.exists()) {
            boolean ok = queueStatFile.createNewFile();
            if (!ok) {
                throw new IOException("创建统计文件失败 queueStatFile" + queueStatFile.getAbsolutePath());
            }
        }
        // 4.设定统计文件默认值
        Stat stat = new Stat();
        stat.totalCount = 0;
        stat.validCount = 0;
        writeStat(queueName, stat);
    }

    // 获取消息的统计结果
    private Stat readStat(String queueName) {
        Stat stat = new Stat();
        try (InputStream inputStream = new FileInputStream(getQueueStatPath(queueName))) {
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    // 将统计结果写入硬盘
    private void writeStat(String queueName, Stat stat) {
        // outputSteam 默认是直接覆盖原有的数值
        try(OutputStream outputStream = new FileOutputStream(getQueueStatPath(queueName))) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(stat.totalCount + "\t" + stat.validCount);
            printWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 文件和目录的删除方法
    public void deleteQueueFiles(String queueName) throws IOException {
        // 删除数据文件
        File queueDataFile = new File(getQueueDataPath(queueName));
        boolean ok1 = queueDataFile.delete();
        // 删除统计文件
        File queueStatFile = new File(getQueueStatPath(queueName));
        boolean ok2 = queueStatFile.delete();
        // 删除目录，只有目录中没有文件才能删除
        File baseDir = new File(getQueueDir(queueName));
        boolean ok3 = baseDir.delete();
        if (!ok1 || !ok2 || !ok3) {
            // 删除失败
            throw new IOException("删除文件及目录失败 baseDir = " + baseDir.getAbsolutePath());
        }
    }

    // 判定文件是否存在
    public boolean checkFilesExists(String queueName) {
        File queueDataFile = new File(getQueueDataPath(queueName));
        if (!queueDataFile.exists()) {
            return false;
        }
        File queueStatFile = new File(getQueueStatPath(queueName));
        return queueStatFile.exists();
    }

    /**
     * 将消息写入队列对应的文件中
     * @param queue 消息要写入的队列
     * @param message 写入的消息
     */
    public void sendMessage(MSGQueue queue, Message message) throws MqException, IOException {
        // 1.检查队列所对应的文件是否存在
        if (!checkFilesExists(queue.getName())) {
            // 消息文件不存在则抛出一个自定义异常
            throw new MqException("[MessageFileManager] 队列对应的文件不存在 queueName = " + queue.getName());
        }
        // 2.将消息进行序列化
        byte[] messageBinary = BinaryTool.toBytes(message);
        // 在写入文件的过程中会有线程安全问题，使用queue作为锁对象进行加锁
        synchronized (queue) {
            // 3.设置消息的起始位置
            File queueDataFile = new File(getQueueDataPath(queue.getName()));
            // 这里的4就是消息长度数据的存放位置
            message.setOffsetBeg(queueDataFile.length() + 4);
            message.setOffsetEnd(queueDataFile.length() + 4 + messageBinary.length);
            // 4.将消息的长度存入文件中 规定4个字节，同时写入消息数据
            // 文件追加的方式写入，不然之前的数据就会丢失
            try (OutputStream outputStream = new FileOutputStream(queueDataFile, true)) {
                // 由于写4个字节而outputStream只能写一个字节 使用DataOutputStream
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    dataOutputStream.writeInt(messageBinary.length);
                    // 将数据写入文件
                    dataOutputStream.write(messageBinary);
                }
            }
            // 5.更新消息统计文件，从消息统计文件读出数据
            Stat stat = readStat(queue.getName());
            if (stat != null) {
                stat.totalCount += 1;
                stat.validCount += 1;
                writeStat(queue.getName(), stat);
            }
        }
    }

    /**
     * 删除对应的消息，这里只需要将isValid修改成0即可
     * @param queue 对应的队列
     * @param message 消息
     */
    public void deleteMessage(MSGQueue queue, Message message) throws IOException, ClassNotFoundException {
        // 有线程安全问题，以消息对象进行加锁
        synchronized (queue) {
            // 1.先从文件中读取文件信息，随机读写
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()), "rw")) {
                // 设置bufferSrc字节数组用来存储消息
                byte[] bufferSrc = new byte[(int) (message.getOffsetEnd() - message.getOffsetBeg())];
                // 将光标定位到起始位置
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.read(bufferSrc);
                // 2.将二进制数据反序列化转换成message对象
                Message diskMessage = (Message) BinaryTool.fromBytes(bufferSrc);
                // 3.将对象中的isValid进行修改
                diskMessage.setIsValid((byte) 0x0);
                // 4.再将对象序列换成二进制数组添加到文件中
                byte[] bufferDest = BinaryTool.toBytes(diskMessage);
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(bufferDest);
                // 5.更新消息统计文件
                Stat stat = readStat(queue.getName());
                if (stat != null && stat.validCount > 0) {
                    stat.validCount -= 1;
                    writeStat(queue.getName(), stat);
                }
            }
        }
    }

    /**
     * 消息重新加载
     * @param queueName 队列名字
     * @return 所有消息
     * @throws IOException 异常
     * @throws MqException 异常
     * @throws ClassNotFoundException 异常
     */
    public LinkedList<Message> loadAllMessagesFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
        LinkedList<Message> messages = new LinkedList<>();
        try (InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))) {
            // 先读取文件中对应消息的长度
            try(DataInputStream dataInputStream = new DataInputStream(inputStream)) {
                int currentOffset = 0;
                while (true) {
                    // 1.读取当前消息的长度
                    int messageSize = dataInputStream.readInt();
                    // 2.根据长度读取消息
                    byte[] buffer = new byte[messageSize];
                    int actualSize = dataInputStream.read(buffer);
                    if (messageSize != actualSize) {
                        throw new MqException("[MessageFileManager] 文件格式错误！queueName = " + queueName);
                    }
                    // 3.将消息反序列化成对象
                    Message message = (Message) BinaryTool.fromBytes(buffer);
                    // 4.判断是否是有效数据
                    if (message.getIsValid() != 0x1) {
                        // 无效数据就不进行加载了
                        // 无效数据也需要记录当前文件光标位置
                        currentOffset += (4 + messageSize);
                        continue;
                    }
                    // 5.将消息对象插入列表中
                    // 插入之前需要设置消息的起始位置
                    message.setOffsetBeg(currentOffset + 4);
                    message.setOffsetEnd(currentOffset + 4 + messageSize);
                    // 更新currentOffset位置
                    currentOffset += (4 + messageSize);
                    messages.add(message);
                }
            } catch (EOFException e) {
                System.out.println("[MessageFileManager] 恢复Message成功！");
            }
        }
        return messages;
    }

    // 检查当前文件时否需要GC
    public boolean checkGC(String queueName) {
        // GC判定条件是消息总数达到2000并且有效消息低于50%
        Stat stat = readStat(queueName);
        return stat.totalCount > 2000 && (double)stat.validCount / (double)stat.totalCount < 0.5;
    }

    // 创建临时新文件用于接收有效消息
    private String getQueueDataNewPath(String queueName) {
        return getQueueDir(queueName) + "/queue_data_new.txt";
    }

    // 实现垃圾回收的具体操作
    public void gc(MSGQueue queue) throws MqException, IOException, ClassNotFoundException {
        synchronized(queue) {
            // 记录操作所需时间
            long gcBeg = System.currentTimeMillis();
            // 1.创建新文件
            File queueDataNewFile = new File(getQueueDataNewPath(queue.getName()));
            if (queueDataNewFile.exists()) {
                // 如果这个替换文件存在，则说明是上次gc出现了问题
                throw new MqException("[MessageFileManager] gc 时该队列queue_data_new.txt文件已经存在 queueName = " + queue.getName());
            }
            // 不存在就创建文件
            Boolean ok = queueDataNewFile.createNewFile();
            if (!ok) {
                throw new MqException("[MessageFileManger] 创建文件失败！ queueDataNewFile=" + queueDataNewFile.getAbsolutePath());
            }
            // 2.从旧文件读出所有有效的消息
            LinkedList<Message> messages = loadAllMessagesFromQueue(queue.getName());
            // 3.将有效消息写入新的临时文件中
            try (OutputStream outputStream = new FileOutputStream(queueDataNewFile)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    // 针对每个消息进行序列化操作
                    for (Message message : messages) {
                        // 序列化操作
                        byte[] buffer = BinaryTool.toBytes(message);
                        // 将消息长度写入
                        dataOutputStream.writeInt(buffer.length);
                        dataOutputStream.write(buffer);
                    }
                }
            }
            // 4.删除原来的文件
            File queueDataOldFile = new File(getQueueDataPath(queue.getName()));
            ok = queueDataOldFile.delete();
            if (!ok) {
                throw new MqException("[MessageFileManager] 文件删除失败！queueDataOldFile = " + queueDataOldFile.getAbsolutePath());
            }
            // 5.将新文件重命名为原文件名
            ok = queueDataNewFile.renameTo(queueDataOldFile);
            if (!ok) {
                throw new MqException("[MessageFileManager] 文件重命名失败！queueDataNewFile = " + queueDataNewFile.getAbsolutePath()+
                        "queueDataOldFile = " + queueDataOldFile.getAbsolutePath());
            }
            // 6.更新统计文件的数据
            Stat stat = readStat(queue.getName());
            stat.totalCount = messages.size();
            stat.validCount = messages.size();
            writeStat(queue.getName(), stat);
            // 统计消耗的时间
            long gcEnd = System.currentTimeMillis();
            System.out.println("[MessageFileManager] gc 执行完毕！queueName = " + queue.getName() +
                    "time = " + (gcEnd - gcBeg) + "ms");
        }
    }
}
